using System;
using System.IO;
using Microsoft.WindowsAzure.StorageClient;
using Microsoft.WindowsAzure.StorageClient.Protocol;

namespace Microsoft.WindowsAzure.Accelerator
{
    public static class CloudPageBlobExtensions
    {
        /// <summary>
        /// Uploads a local VHD (virutal hard drive) file to an Azure cloud page blob (cloud drive).
        /// </summary>
        /// <remarks>
        /// Azure provisioning on a newly crated PagBlob will ensure that all zeros exist.
        /// 
        /// Given this, any upload of a local VHD can be minimized by limiting transfer to 
        /// only those pages (512 byte segments) which contain dirty bits. Also, want to avoid 
        /// a bunch of small upload batches of 512b, so simply testing each page for empty and 
        /// immediatly acting on the uploads in place isnt a good idea.
        /// 
        /// The code below attempts to queue up the largest possible block of dirty pages and
        /// then write them all at once.  (Well, more correctly, the largest block in each buffer
        /// window.) 
        /// 
        /// It does this by looking ahead for the next empty page. Everything in between
        /// two empty pages is dirty, and an upload begins. If each successive page is empty
        /// it just keeps incrementing the offsets without uploading anything.
        /// 
        /// Sure, disk fragmentation may result in massive tiny uploads instead of blocks, but
        /// considering the nature of a VHD, this should be the exception to the rule, with 
        /// dirty and empty areas of the disk grouped together. (i|rdm)
        /// </remarks>
        /// <param name="blob">The target blob (cloud drive).</param>
        /// <param name="vhd">The local VHD file (virtual hard drive).</param>
        /// <returns>Size uploaded.</returns>
        public static Int64 UploadCloudDrive(this CloudPageBlob blob, FileInfo vhd)
        {
            const Int32 _1Kb = 1024;
            const Int32 _1Mb = _1Kb * _1Kb;
            const Int32 _4Mb = _1Mb * 4;
            const Int32 pageSize = 512;
            const Int32 bufferSize = _4Mb;
            const Int32 pagesInBuffer = bufferSize / pageSize;

            //i| Provision the Cloud Drive.
            Int64 cloudDriveSize = (Int32)Math.Ceiling((double)vhd.Length / pageSize) * pageSize;
            blob.Create(cloudDriveSize);

            //i| Set our local resources.
            FileStream stream = vhd.OpenRead();
            Byte[] buffer = new Byte[bufferSize];
            Int64 vhdOffset = 0;

            while (buffer.Fill(stream) > 0)
            {
                Int32 prevEmpty = -1;
                Int32 nextEmpty = 0;

                do
                {
                    nextEmpty = buffer.SeekNextEmpty(pageSize, prevEmpty);
                    if (nextEmpty - prevEmpty > 1)
                    {
                        Int32 byteOffset = (prevEmpty + 1) * pageSize;
                        blob.WritePages(new MemoryStream(buffer, byteOffset, (nextEmpty - prevEmpty) * pageSize), vhdOffset + byteOffset);
                    }
                    prevEmpty = nextEmpty;
                } while (nextEmpty < pagesInBuffer);

                //i| Status
                Console.WriteLine("{0,4} of {1} Mb ( {2,5:P2} )", vhdOffset / _1Mb, cloudDriveSize / _1Mb, ((Double)vhdOffset / cloudDriveSize));

                vhdOffset += bufferSize;
            }

            //i| Success
            Console.WriteLine("Blob successfully uploaded.");

            //i| Create snapshot.
            var snapshot = blob.CreateSnapshot();
            if (snapshot.SnapshotTime != null)
                Console.WriteLine("Snapshot created:  '{0}'", BlobRequest.Get(snapshot.Uri, 0, snapshot.SnapshotTime.Value.ToUniversalTime(), null).RequestUri.AbsoluteUri);
            else
                Console.WriteLine("Unable to create snapshot of uploaded page blob.");

            return vhdOffset;
        }

        /// <summary>
        /// Determines whether a page in the provided buffer contains all zeros.
        /// </summary>
        /// <param name="buffer">The buffer.</param>
        /// <param name="pageSize">Size of the page.</param>
        /// <param name="pageNumber">The page number to check for empty.</param>
        /// <returns>
        /// 	<c>true</c> if page in the buffer is empty; otherwise, <c>false</c>.
        /// </returns>
        public static Boolean IsPageEmpty(this Byte[] buffer, Int32 pageSize, Int32 pageNumber)
        {
            Int32 byteOffset = pageNumber * pageSize;
            Int32 pageEnd = byteOffset + pageSize;
            do
            {
                if (buffer[byteOffset++] != 0) return false;
            } while (byteOffset < pageEnd);
            return true;
        }

        /// <summary>
        /// Fills the specified buffer with content from the supplied stream.
        /// </summary>
        /// <param name="buffer">The buffer.</param>
        /// <param name="stream">The stream.</param>
        /// <returns>Number of bytes read from the buffer.</returns>
        public static Int32 Fill(this Byte[] buffer, FileStream stream)
        {
            Int32 count = stream.Read(buffer, 0, buffer.Length);
            Array.Clear(buffer, count, buffer.Length);
            return count;
        }

        /// <summary>
        /// Seeks the next empty.
        /// </summary>
        /// <param name="buffer">The buffer to search.</param>
        /// <param name="pageSize">Size of the pages.</param>
        /// <param name="pageIndex">The index of the page to start from.</param>
        /// <returns>The index of the next empty page. If no empty page found the page after the end of the buffer.</returns>
        public static Int32 SeekNextEmpty(this Byte[] buffer, Int32 pageSize, Int32 pageIndex)
        {
            do {
            } while (!buffer.IsPageEmpty(pageSize, ++pageIndex) && pageIndex < (buffer.Length / pageSize));
            return pageIndex;
        }

    }
}